package utilities;

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

public class JSONParser implements Parser {
    // Instance Variables
	private int index;
	private String json;
	
	// JSON Whitespace Tokens
	public static final char TOKEN_SPACE = ' ';
	public static final char TOKEN_FORMFEED = '\f';
	public static final char TOKEN_NEWLINE = '\n';
	public static final char TOKEN_CARRIAGE_RETURN = '\r';
	public static final char TOKEN_HORIZONTAL_TAB = '\t';
	
	// JSON Tokens
	public static final char TOKEN_OPEN_BRACE = '{';
	public static final char TOKEN_CLOSE_BRACE = '}';
	public static final char TOKEN_OPEN_BRACKET = '[';
	public static final char TOKEN_CLOSE_BRACKET = ']';
	public static final char TOKEN_COLON = ':';
	public static final char TOKEN_PERIOD = '.';
	public static final char TOKEN_COMMA = ',';
	public static final char TOKEN_NONE = '\0';
	public static final char TOKEN_QUOTATION_MARK = '\"';
	public static final char TOKEN_TRUE = 't';
	public static final char TOKEN_FALSE = 'f';
	public static final char TOKEN_NULL = 'n';
	public static final char TOKEN_BACKSLASH = '\\';
	public static final char TOKEN_FORWARD_SLASH = '/';
	public static final char TOKEN_BACKSPACE = '\b';
	public static final char TOKEN_HEX_CHARACTER = 'u';
	public static final char TOKEN_POSITIVE_SIGN = '+';
	public static final char TOKEN_NEGATIVE_SIGN = '-';
	public static final char TOKEN_DIGIT_ONE = '1';
	public static final char TOKEN_DIGIT_TWO = '2';
	public static final char TOKEN_DIGIT_THREE = '3';
	public static final char TOKEN_DIGIT_FOUR = '4';
	public static final char TOKEN_DIGIT_FIVE = '5';
	public static final char TOKEN_DIGIT_SIX = '6';
	public static final char TOKEN_DIGIT_SEVEN = '7';
	public static final char TOKEN_DIGIT_EIGHT = '8';
	public static final char TOKEN_DIGIT_NINE = '9';
	public static final char TOKEN_DIGIT_ZERO = '0';
	public static final char TOKEN_EXPONENTIAL = 'e';
	public static final char TOKEN_EXPONENTIAL_UPPER = 'E';
	
	
	// Constructor
	/**
	 * Constructor: sets the index to 0 and cleans the jsonString
	 */
	public JSONParser(String jsonString) {
		this.index = 0;
		this.json = cleanedJSONString(jsonString);
	}
	
	
	// Archive methods
	/**
	 * Creates an archive from the map
	 */
	public static Archive createArchive(Map<String, Object> map) {
		// Get hashmaps
		@SuppressWarnings("unchecked")
		Map<String, String> attributes = (Map<String, String>)map.get(JSONArchive.ATTRIBUTES_KEY);
		@SuppressWarnings("unchecked")
		Map<String, List<String>> attributeLists = (Map<String, List<String>>)map.get(JSONArchive.ATTRIBUTE_LISTS_KEY);
		@SuppressWarnings("unchecked")
		Map<String, Object> one = (Map<String, Object>)map.get(JSONArchive.ONE_KEY);
		@SuppressWarnings("unchecked")
		Map<String, List<Object>> many = (Map<String, List<Object>>)map.get(JSONArchive.MANY_KEY);
		
		// Create a new Archive
		JSONArchive archive = new JSONArchive();
		
		// Put attributes
		if (attributes != null) {
			for (String key : attributes.keySet()) {
				String value = attributes.get(key);
				archive.putAttribute(key, value);
			}
		}
		
		// Put attribute lists
		if (attributeLists != null) {
			for (String key : attributeLists.keySet()) {
				List<String> value = attributeLists.get(key);
				archive.putAttributeList(key, value);
			}
		}
		
		// Put one-one relationships
		if (one != null) {
			for (String key : one.keySet()) {
				@SuppressWarnings("unchecked")
				Map<String, Object> value = (Map<String, Object>)one.get(key);
				Archive oneArchive = createArchive(value);
				archive.oneRelationships.put(key, oneArchive);
			}
		}
		
		// Put one-many relationships
		if (many != null) {
			for (String key : many.keySet()) {
				List<Object> value = many.get(key);
				
				List<Archive> archives = new ArrayList<Archive>(value.size());
				for (int i = 0; i < value.size(); i++) {
					@SuppressWarnings("unchecked")
					Map<String, Object> valueMap = (Map<String, Object>)value.get(i);
					
					archives.add(createArchive(valueMap));
				}
				archive.manyRelationships.put(key, archives);
			}
		}
		
		return archive;
	}
	
	
	// Parse methods
	/**
	 * Parses the JSON string into an Archive
	 */
	public Archive parse() {
		this.index = 0;
		Map<String, Object> data = this.parseObject();
		return createArchive(data);
	}
	
	private Map<String, Object> parseObject() {
		Map<String, Object> object = new HashMap<String, Object>();
		
		// '{'
		if (!this.testNextCharacterEqualTo(TOKEN_OPEN_BRACE)) {
		    return null;
		}
		this.consumeNextCharacter();
		
		// Loop through each Key-Value pair
		boolean done = false;
		while (!done) {
            char token = this.peekNextCharacter();
            switch(token) {
                case TOKEN_CLOSE_BRACE: {
                    // Done with this object
                    done = true;
                    break;
                }
                case TOKEN_NONE: {
                    // Premature end of file
                    return null;
                }
                case TOKEN_COMMA: {
                    // Another nested Key-Value pair
                    this.consumeNextCharacter();
                    break;
                }
                default: {
                    // Key-Value pair
                    // Key
                    String key = this.parseString();
                    if (key == null) {
                        return null;
                    }
                    
                    // ':'
                    char colon = this.consumeNextCharacter();
                    if (!testCharacterEquality(colon, TOKEN_COLON)) {
                        return null;
                    }
                    
                    // Value
                    Object value = this.parseValue();
                    if (value == null) {
                        return null;
                    }
                    
                    // Add the pair
                    object.put(key, value);
                    break;
                }
            }
		}
		return object;
	}
	
	private List<Object> parseArray() {
	    List<Object> list = new ArrayList<Object>();
	    
	    // '['
		char leftBrace = this.consumeNextCharacter();
		if (!testCharacterEquality(leftBrace, TOKEN_OPEN_BRACKET)) {
		    return null;
		}
		
		// Loop through each Value
		boolean done = false;
		while (!done) {
		    char token = this.peekNextCharacter();
		    switch(token) {
		        case TOKEN_CLOSE_BRACKET: {
		            // Done with this array
		            this.consumeNextCharacter();
		            done = true;
		            break;
		        }
		        case TOKEN_COMMA: {
		            // Another Value
		            this.consumeNextCharacter();
		            break;
		        }
		        case TOKEN_NONE: {
		            // Premature end of file
		            return null;
		        }
		        default: {
		            // Parse and add the value
		            Object value = this.parseValue();
		            list.add(value);
		            break;
		        }
		    }
		}
		return list;
	}
	
	private Object parseValue() {
	    Object value = null;
	    
	    char token = this.peekNextCharacter();
	    
	    switch(token) {
	        case TOKEN_NONE: {
	            // Premature end of file
	            break;
	        }
	        case TOKEN_OPEN_BRACE: {
	            // Object
	            value = this.parseObject();
	            this.consumeNextCharacter();
	            break;
	        }
	        case TOKEN_OPEN_BRACKET: {
	            // Array
	            value = this.parseArray();
	            break;
	        }
	        case TOKEN_NEGATIVE_SIGN:
	        case TOKEN_DIGIT_ONE:
	        case TOKEN_DIGIT_TWO:
	        case TOKEN_DIGIT_THREE:
	        case TOKEN_DIGIT_FOUR:
	        case TOKEN_DIGIT_FIVE:
	        case TOKEN_DIGIT_SIX:
	        case TOKEN_DIGIT_SEVEN:
	        case TOKEN_DIGIT_EIGHT:
	        case TOKEN_DIGIT_NINE:
	        case TOKEN_DIGIT_ZERO: {
	            // Number
	            value = this.parseNumber();
	            break;
	        }
	        case TOKEN_QUOTATION_MARK: {
	            // String
	            value = this.parseString();
	            break;
	        }
	        case TOKEN_TRUE: {
	            // True value
	            value = this.parseTrue();
	            break;
	        }
	        case TOKEN_FALSE: {
	            // False value
	            value = this.parseFalse();
	            break;
	        }
	        case TOKEN_NULL: {
	            // Null value
	            value = this.parseNull();
	            break;
	        }
	    }
	    return value;
	}
	
	private String parseString() {
	    StringBuilder builder = new StringBuilder();
	    
	    // '"'
	    char quotationMark = this.consumeNextCharacter();
	    if (!testCharacterEquality(quotationMark, TOKEN_QUOTATION_MARK)) {
	        return null;
	    }
	    
	    // Loop through each character
	    while (true) {
	        char nextCharacter = this.consumeNextCharacter();
	        
	        // Test premature end of file
	        if (testCharacterEquality(nextCharacter, TOKEN_NONE)) {
	            return null;
	        }
	        
	        // Test end of string
	        if (testCharacterEquality(nextCharacter, TOKEN_QUOTATION_MARK)) {
	            break;
	        }
	        
	        builder.append(nextCharacter);
	    }
	    return builder.toString();
	}
	
	private Number parseNumber() {
	    StringBuilder builder = new StringBuilder();
	    boolean isDouble = false;
	    
	    // '-'
	    if (this.testNextCharacterEqualTo(TOKEN_NEGATIVE_SIGN)) {
	        char negativeSign = this.consumeNextCharacter();
	        builder.append(negativeSign);
	    }
	    
	    // Number starts with '0'
	    if (this.testNextCharacterEqualTo(TOKEN_DIGIT_ZERO)) {
	        char zero = this.consumeNextCharacter();
	        builder.append(zero);
	    } else {
	        // Parse the first digit of the number
	        char digit = this.consumeNextCharacter();
	        builder.append(digit);
	        
	        // Parse remaining digits from main number
	        while (true) {
	            char next = this.peekNextCharacter();
	            
	            // Use the Integer.parseInt() function to test if next character is a digit
                try {
                    @SuppressWarnings("unused")
					int test = Integer.parseInt(Character.toString(next));
                    
                    digit = this.consumeNextCharacter();
                    builder.append(digit);
                } catch (NumberFormatException e) {
                    // If the integer parsing fails, we are done with the main number loop
                    break;
                }
	        }
	    }
	    
        // '.'
        if (testNextCharacterEqualTo(TOKEN_PERIOD)) {
            char period = this.consumeNextCharacter();
            builder.append(period);
            
            while (true) {
	            char next = this.peekNextCharacter();
	            
	            // Use the Integer.parseInt() function to test if next character is a digit
                try {
                    @SuppressWarnings("unused")
					int test = Integer.parseInt(Character.toString(next));
                    
                    char digit = this.consumeNextCharacter();
                    builder.append(digit);
                } catch (NumberFormatException e) {
                    // If the integer parsing fails, we are done with the main number loop
                    break;
                }
	        }
        }
	    
	    // 'e' or 'E'
	    if (this.testNextCharacterEqualTo(TOKEN_EXPONENTIAL) || this.testNextCharacterEqualTo(TOKEN_EXPONENTIAL_UPPER)) {
	        char exponential = this.consumeNextCharacter();
	        builder.append(exponential);
	        
	        // '+' or '-'
	        if (testNextCharacterEqualTo(TOKEN_POSITIVE_SIGN)) {
	            char positiveSign = this.consumeNextCharacter();
	            builder.append(positiveSign);
	        } else if (testNextCharacterEqualTo(TOKEN_NEGATIVE_SIGN)) {
	            char negativeSign = this.consumeNextCharacter();
	            builder.append(negativeSign);
	        }
	        
	        // Exponent
	        while (true) {
	            char next = this.peekNextCharacter();
	            
	            // Use the Integer.parseInt() function to test if next character is a digit
                try {
                    @SuppressWarnings("unused")
					int test = Integer.parseInt(Character.toString(next));
                    
                    char digit = this.consumeNextCharacter();
                    builder.append(digit);
                } catch (NumberFormatException e) {
                    // If the integer parsing fails, we are done with the main number loop
                    break;
                }
	        }
	    }
	    
	    // Determine which number return
	    String number = builder.toString();
	    if (isDouble) {
	        try {
	            return Double.parseDouble(number);
	        } catch (NumberFormatException e) {
	            return null;
	        }
	    } else {
	        try {
	            return Integer.parseInt(number);
	        } catch (NumberFormatException e) {
	            return null;
	        }
	    }
	}
	
	private Boolean parseTrue() {
	    StringBuilder builder = new StringBuilder();
	    
	    // 't'
	    if (!testNextCharacterEqualTo('t')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'r'
	    if (!testNextCharacterEqualTo('r')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'u'
	    if (!testNextCharacterEqualTo('u')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'e'
	    if (!testNextCharacterEqualTo('e')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    String bool = builder.toString();
	    return Boolean.getBoolean(bool);
	}
	
	private Boolean parseFalse() {
	    StringBuilder builder = new StringBuilder();
	    
	    // 'f'
	    if (!testNextCharacterEqualTo('f')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'a'
	    if (!testNextCharacterEqualTo('a')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'l'
	    if (!testNextCharacterEqualTo('l')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 's'
	    if (!testNextCharacterEqualTo('s')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'e'
	    if (!testNextCharacterEqualTo('e')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    String bool = builder.toString();
	    return Boolean.getBoolean(bool);
	}
	
	private String parseNull() {
	    StringBuilder builder = new StringBuilder();
	    
	    // 'n'
	    if (!testNextCharacterEqualTo('n')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'u'
	    if (!testNextCharacterEqualTo('u')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'l'
	    if (!testNextCharacterEqualTo('l')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    // 'l'
	    if (!testNextCharacterEqualTo('l')) {
	        return null;
	    }
	    builder.append(this.consumeNextCharacter());
	    
	    return builder.toString();
	}
	
	// Utilities
	private char peekNextCharacter() {
	    if (this.index >= this.json.length()) {
	        return '\0';
	    }
	    return this.json.charAt(this.index);
	}
	
	private char consumeNextCharacter() {
	    if (this.index >= this.json.length()) {
	        return '\0';
	    }
	    return this.json.charAt(this.index++);
	}
	
	private static boolean testCharacterEquality(char a, char b) {
	    return a == b;
	}
	
	private boolean testNextCharacterEqualTo(char c) {
        return this.peekNextCharacter() == c;
	}
	
	private boolean nextCharacterIsWhitespace(char c) {
		boolean whitespace = false;
		
		if (c == TOKEN_SPACE) {
			whitespace = true;
		} else if (c == TOKEN_FORMFEED) {
			whitespace = true;
		} else if (c == TOKEN_NEWLINE) {
			whitespace = true;
		} else if (c == TOKEN_CARRIAGE_RETURN) {
			whitespace = true;
		} else if (c == TOKEN_HORIZONTAL_TAB) {
			whitespace = true;
		}
		
		return whitespace;
	}
	
	
	// Clean methods
	/**
	 * Cleans the JSON string, removing any whitespace characters
	 */
	private String cleanedJSONString(String string) {
		int index = 0;
		int maxIndex = string.length();
		StringBuilder clean = new StringBuilder();
		
		while (index < maxIndex) {
			char c = string.charAt(index++);
			
			if (c == TOKEN_QUOTATION_MARK) {
				clean.append(c);
				while (index < maxIndex) {
					c = string.charAt(index++);
					clean.append(c);
					if (c == TOKEN_QUOTATION_MARK) {
						break;
					}
				}
			} else if (nextCharacterIsWhitespace(c)) {
				// do nothing, don't add to cleaned string
			} else {
				clean.append(c);
			}
		}
		
		return clean.toString();
	}
	
}

